package k8.util;

import k8.SceneNode;

import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;

public final class Camera extends SceneNode
{
	// The single instance of the Camera
	private static Camera instance = null;

	// Field of view (x-axis)
	private int fov; 

	// Determines field of view (y-axis)
	private float aspectRatio; 

	// Near clipping plane distance
	private float nearClip; 

	// Far clipping plane distance
	private float farClip; 

	/**
	 * Gets the Camera instance.
	 */
	public static Camera getInstance()
	{
		if (instance == null)
			instance = new Camera();
		return instance;
	}

	/** Enforce Singleton. */
	private Camera()
	{
		// Default at 40 degrees field of view (x-axis)
		fov = 40;

		// Default aspect ratio of the window
		DisplayMode dm = Display.getDisplayMode();
		aspectRatio = dm.getWidth() / dm.getHeight();

		// Default clipping planes (always positive, near never less than 0.1)
		nearClip = 1f;
		farClip = 10000;
		setProjectionMatrix();
	}

	/** Sets the field of view (x-axis) */
	public void setFOV(int fov)
	{
		this.fov = fov;
		setProjectionMatrix();
	}

	/** Gets the field of view (x-axis) */
	public int getFOV()
	{
		return fov;
	}

	/** Sets the aspect ratio */
	public void setAspectRatio(float ratio)
	{
		aspectRatio = ratio;
		setProjectionMatrix();
	}

	/** Gets the aspect ratio. Use to determine fov on y-axis. */
	public float getAspectRatio()
	{
		return aspectRatio;
	}

	/** Sets the near clipping plane distance */
	public void setNearClip(float nearClip)
	{
		this.nearClip = nearClip;
		setProjectionMatrix();
	}

	/** Gets the near clipping plane distance */
	public float getNearClip()
	{
		return nearClip;
	}

	/** Sets the far clipping plane distance */
	public void setFarClip(float farClip)
	{
		this.farClip = farClip;
		setProjectionMatrix();
	}

	/** Gets the far clipping plane distance */
	public float getFarClip()
	{
		return farClip;
	}

	/**
	 * Sets the perspective projection matrix
	 */
	private void setProjectionMatrix()
	{
		float radians = (float) (fov / 2 * Math.PI / 180);
		float deltaZ = farClip - nearClip;
		float sine = (float) Math.sin(radians);
		float cotangent = (float) Math.cos(radians) / sine;

		// Sanity check
		if ((deltaZ == 0) || (sine == 0) || (aspectRatio == 0)) {
			return;
		}
		
		// Select the projection matrix
		GL11.glMatrixMode(GL11.GL_PROJECTION);

		// Reset the projection matrix
		GL11.glLoadIdentity();

		// Set the projection matrix to use perspective
		Matrix.makeIdentity(Matrix.matrix);		
		Matrix.matrix.put(0 * 4 + 0, cotangent / aspectRatio);		
		Matrix.matrix.put(1 * 4 + 1, cotangent);		
		Matrix.matrix.put(2 * 4 + 2, - (farClip + nearClip) / deltaZ);
		Matrix.matrix.put(2 * 4 + 3, -1);		
		Matrix.matrix.put(3 * 4 + 2, -2 * nearClip * farClip / deltaZ);
		Matrix.matrix.put(3 * 4 + 3, 0);

		GL11.glMultMatrix(Matrix.matrix);
		
		// Select the modelview matrix
		GL11.glMatrixMode(GL11.GL_MODELVIEW);
	}

	@Override
	protected void transform()
	{
		Matrix.makeIdentity(Matrix.matrix);
		
		Matrix.matrix.put(0 * 4 + 0, this.T[0]);
		Matrix.matrix.put(1 * 4 + 0, this.T[4]);
		Matrix.matrix.put(2 * 4 + 0, this.T[8]);

		Matrix.matrix.put(0 * 4 + 1, this.T[1]);
		Matrix.matrix.put(1 * 4 + 1, this.T[5]);
		Matrix.matrix.put(2 * 4 + 1, this.T[9]);

		Matrix.matrix.put(0 * 4 + 2, this.T[2]);
		Matrix.matrix.put(1 * 4 + 2, this.T[6]);
		Matrix.matrix.put(2 * 4 + 2, this.T[10]);

		GL11.glLoadIdentity();
		GL11.glMultMatrix(Matrix.matrix);
		GL11.glTranslated(-this.T[3], -this.T[7], -this.T[11]);
	}

}
